﻿using System;
using System.Globalization;

namespace LinqBridge.InternalHelpers
{
	/// <summary>
	/// Implementation for early Mono runtime
	/// </summary>
	class MonoConversion : IConversion
	{
		public DateTimeOffset? ToDateTimeOffset(string value)
		{
			return DateTimeOffset.ParseExact(
				value, datetimeFormats, CultureInfo.InvariantCulture,
				DateTimeStyles.AssumeUniversal);
		}

		public TimeSpan ToTimeSpanHelper(string s)
		{
			s = s.Trim(XmlChar.WhitespaceChars);
			if (s.Length == 0)
				throw new FormatException("Invalid format string for duration schema datatype.");

			int start = 0;
			if (s[0] == '-')
				start = 1;
			bool minusValue = (start == 1);

			if (s[start] != 'P')
				throw new FormatException("Invalid format string for duration schema datatype.");
			start++;

			int parseStep = 0;
			int days = 0;
			bool isTime = false;
			int hours = 0;
			int minutes = 0;
			int seconds = 0;
			long ticks = 0;
			int parsedDigits = 0;

			bool error = false;

			int i = start;
			while (i < s.Length)
			{
				if (s[i] == 'T')
				{
					isTime = true;
					parseStep = 4;
					i++;
					start = i;
					continue;
				}
				for (; i < s.Length; i++)
					if (s[i] < '0' || '9' < s[i])
						break;
				if (parseStep == 7)
					parsedDigits = i - start;
				int value = int.Parse(s.Substring(start, i - start), CultureInfo.InvariantCulture);
				if (parseStep == 7)
				{
					// adjust to 7 digits so that it makes sense as millisecond digits
					for (; parsedDigits > 7; parsedDigits--)
						value /= 10;
					for (; parsedDigits < 7; parsedDigits++)
						value *= 10;
				}
				switch (s[i])
				{
					case 'Y':
						days += value * 365;
						if (parseStep > 0)
							error = true;
						else
							parseStep = 1;
						break;
					case 'M':
						if (parseStep < 2)
						{
							days += 365 * (value / 12) + 30 * (value % 12);
							parseStep = 2;
						}
						else if (isTime && parseStep < 6)
						{
							minutes = value;
							parseStep = 6;
						}
						else
							error = true;
						break;
					case 'D':
						days += value;
						if (parseStep > 2)
							error = true;
						else
							parseStep = 3;
						break;
					case 'H':
						hours = value;
						if (!isTime || parseStep > 4)
							error = true;
						else
							parseStep = 5;
						break;
					case 'S':
						if (parseStep == 7)
							ticks = value;
						else
							seconds = value;
						if (!isTime || parseStep > 7)
							error = true;
						else
							parseStep = 8;
						break;
					case '.':
						if (parseStep > 7)
							error = true;
						seconds = value;
						parseStep = 7;
						break;
					default:
						error = true;
						break;
				}
				if (error)
					break;
				++i;
				start = i;
			}
			if (error)
				throw new FormatException("Invalid format string for duration schema datatype.");

			TimeSpan ts = new TimeSpan(days, hours, minutes, seconds);
			if (minusValue)
				return TimeSpan.FromTicks(-(ts.Ticks + ticks));

			return TimeSpan.FromTicks(ts.Ticks + ticks);
		}

		public float ToSingle(string s)
		{
			if (s == null)
				throw new ArgumentNullException();

			float f = TryParseStringFloatConstants(s);
			if (f != 0)
				return f;

			return Single.Parse(s, floatStyle, CultureInfo.InvariantCulture);
		}

		public double ToDouble(string s)
		{
			if (s == null)
				throw new ArgumentNullException();

			float f = TryParseStringFloatConstants(s);
			if (f != 0)
				return f;

			return Double.Parse(s, floatStyle, CultureInfo.InvariantCulture);
		}

		private float TryParseStringFloatConstants(string s)
		{
			int sidx = 0;
			while (sidx < s.Length && Char.IsWhiteSpace(s[sidx]))
				sidx++;
			if (sidx == s.Length)
				throw new FormatException();
			int sEndPos = s.Length - 1;
			while (Char.IsWhiteSpace(s[sEndPos]))
				sEndPos--;

			if (TryParseStringConstant("NaN", s, sidx, sEndPos))
				return Single.NaN;
			if (TryParseStringConstant("INF", s, sidx, sEndPos))
				return Single.PositiveInfinity;
			if (TryParseStringConstant("-INF", s, sidx, sEndPos))
				return Single.NegativeInfinity;
			// Handle these here because Single.Parse("Infinity")
			// is invalid while XmlConvert.ToSingle("Infinity") is valid.
			if (TryParseStringConstant("Infinity", s, sidx, sEndPos))
				return Single.PositiveInfinity;
			if (TryParseStringConstant("-Infinity", s, sidx, sEndPos))
				return Single.NegativeInfinity;
			return 0;
		}

		private bool TryParseStringConstant(string format, string s, int start, int end)
		{
			return end - start + 1 == format.Length && String.CompareOrdinal(
				format, 0, s, start, format.Length) == 0;
		}

		const NumberStyles floatStyle = NumberStyles.AllowCurrencySymbol |
			NumberStyles.AllowExponent |
				NumberStyles.AllowDecimalPoint |
					NumberStyles.AllowLeadingSign |
						NumberStyles.AllowLeadingWhite |
							NumberStyles.AllowTrailingWhite;

		private readonly string[] datetimeFormats = {
			// dateTime
			"yyyy-MM-ddTHH:mm:sszzz",
			"yyyy-MM-ddTHH:mm:ss.FFFFFFFzzz",
			"yyyy-MM-ddTHH:mm:ssZ",
			"yyyy-MM-ddTHH:mm:ss.FFFFFFFZ",
			"yyyy-MM-ddTHH:mm:ss",
			"yyyy-MM-ddTHH:mm:ss.FFFFFFF",
			"HH:mm:ss",
			"HH:mm:ss.FFFFFFF",
			"HH:mm:sszzz",
			"HH:mm:ss.FFFFFFFzzz",
			"HH:mm:ssZ",
			"HH:mm:ss.FFFFFFFZ",

			// date
			"yyyy-MM-dd",
			"yyyy-MM-ddzzz",
			"yyyy-MM-ddZ",
			// gYearMonth
			"yyyy-MM",
			"yyyy-MMzzz",
			"yyyy-MMZ",
			// gYear
			"yyyy",
			"yyyyzzz",
			"yyyyZ",
			// gMonthDay
			"--MM-dd",
			"--MM-ddzzz",
			"--MM-ddZ",
			// gDay
			"---dd",
			"---ddzzz",
			"---ddZ",
		};
	}
}